package org.example.pinda.database.datasource;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ArrayUtil;
import com.baomidou.mybatisplus.autoconfigure.ConfigurationCustomizer;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusProperties;
import com.baomidou.mybatisplus.autoconfigure.MybatisPlusPropertiesCustomizer;
import com.baomidou.mybatisplus.autoconfigure.SpringBootVFS;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.mapping.DatabaseIdProvider;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandler;
import org.example.pinda.database.properties.DatabaseProperties;
import org.springframework.aop.Advisor;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.MethodMatcher;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.stereotype.Controller;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.interceptor.*;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RestController;

import javax.sql.DataSource;
import java.lang.annotation.Annotation;
import java.util.*;

/**
 * 数据库 & 事务 & MyBatis & MyBatis Plus 配置
 */
@Slf4j
public abstract class BaseDatabaseConfiguration implements InitializingBean {

    @Value("${spring.profiles.active:dev}")
    protected String profiles;
    /**
     * MyBatis Plus 配置属性
     */
    protected MybatisPlusProperties mybatisPlusProperties;
    /**
     * 测试环境
     */
    protected static final String[] DEV_PROFILES = new String[]{"dev"};
    /**
     * 数据库配置
     */
    private DatabaseProperties databaseProperties;
    /**
     * 数据库拦截器
     */
    private Interceptor[] interceptors;
    /**
     * 类型处理器
     */
    private TypeHandler<?>[] typeHandlers;
    /**
     * 语言驱动
     */
    private LanguageDriver[] languageDrivers;
    /**
     * 资源加载器
     */
    private ResourceLoader resourceLoader;
    /**
     * 数据库Id提供器
     */
    private DatabaseIdProvider databaseIdProvider;
    /**
     * 配置定制器
     */
    private List<ConfigurationCustomizer> configurationCustomizers;
    /**
     * MyBatis Plus属性定制器
     */
    private List<MybatisPlusPropertiesCustomizer> mybatisPlusPropertiesCustomizers;
    /**
     * Spring 上下文
     */
    private ApplicationContext applicationContext;

    private static final List<Class<? extends Annotation>> AOP_POINTCUT_ANNOTATIONS
            = new ArrayList<>(2);

    // 事务在Controller层开启
    static {
        AOP_POINTCUT_ANNOTATIONS.add(Controller.class);
        AOP_POINTCUT_ANNOTATIONS.add(RestController.class);
    }

    /**
     * 事务源处理，当前存在事务就使用当前的事务，如果不存在事务就新建一个事务
     *
     * @return {@link TransactionAttributeSource}
     */
    protected TransactionAttributeSource transactionAttributeSource() {
        Map<String, TransactionAttribute> txMap = buildTxMap();
        txMap.put("*", buildReadOnlyTx());

        NameMatchTransactionAttributeSource nameMatchTransactionAttributeSource =
                new NameMatchTransactionAttributeSource();
        nameMatchTransactionAttributeSource.setNameMap(txMap);
        return nameMatchTransactionAttributeSource;
    }

    /**
     * 根据数据库属性配置构建事务属性集合
     *
     * @return 事务属性集合
     */
    private Map<String, TransactionAttribute> buildTxMap() {
        RuleBasedTransactionAttribute requiredTx = buildRuleBasedTxAttribute();

        List<String> transactionAttributes = this.databaseProperties.getTransactionAttributes();

        Map<String, TransactionAttribute> txMap = new HashMap<>(transactionAttributes.size() + 5);
        transactionAttributes.forEach(prefix -> txMap.put(prefix, requiredTx));
        return txMap;
    }

    /**
     * 构建只读的事务
     *
     * @return 只读事务
     */
    private RuleBasedTransactionAttribute buildReadOnlyTx() {
        RuleBasedTransactionAttribute readOnlyTx = new RuleBasedTransactionAttribute();
        readOnlyTx.setReadOnly(true);
        readOnlyTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_SUPPORTS);
        return readOnlyTx;
    }

    /**
     * 构建事务规则
     *
     * @return {@link RuleBasedTransactionAttribute}
     */
    private RuleBasedTransactionAttribute buildRuleBasedTxAttribute() {
        RuleBasedTransactionAttribute requiredTx = new RuleBasedTransactionAttribute();
        requiredTx.setRollbackRules(Collections.singletonList(new RollbackRuleAttribute(Throwable.class)));
        requiredTx.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        requiredTx.setTimeout(this.databaseProperties.getTxTimeout());
        return requiredTx;
    }

    protected Advisor txAdviceAdvisor(TransactionInterceptor interceptor) {
        return new DefaultPointcutAdvisor(new Pointcut() {
            @NonNull
            @Override
            public ClassFilter getClassFilter() {
                return cls -> {
                    if (!cls.getName().startsWith(BaseDatabaseConfiguration.this.databaseProperties.getTransactionScanPackage())) {
                        return false;
                    }
                    for (Class<? extends Annotation> aClass : AOP_POINTCUT_ANNOTATIONS) {
                        if (cls.getAnnotation(aClass) == null) {
                            continue;
                        }
                        log.warn("允许带事务的类：{}", cls);
                        return true;
                    }
                    return false;
                };
            }

            @NonNull
            @Override
            public MethodMatcher getMethodMatcher() {
                return MethodMatcher.TRUE;
            }
        }, interceptor);
    }

    protected SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);

        this.applyMyBatisPlusProperties(factory);

        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }

        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(databaseIdProvider);
        }

        if (!ObjectUtils.isEmpty(this.typeHandlers)) {
            factory.setTypeHandlers(this.typeHandlers);
        }

        if (!ObjectUtils.isEmpty(this.languageDrivers)) {
            factory.setScriptingLanguageDrivers(this.languageDrivers);
        }
        // 设置全局配置
        factory.setGlobalConfig(buildMyBatisGlobalConfig());
        return factory.getObject();
    }

    private GlobalConfig buildMyBatisGlobalConfig() {
        GlobalConfig globalConfig =
                this.mybatisPlusProperties.getGlobalConfig();
        // 注入填充器
        if (this.applicationContext.getBeanNamesForType(MetaObjectHandler.class, false, false).length > 0) {
            globalConfig.setMetaObjectHandler(this.applicationContext.getBean(MetaObjectHandler.class));
        }

        // 注入主键生成器
        if (this.applicationContext.getBeanNamesForType(IKeyGenerator.class, false, false).length > 0) {
            globalConfig.getDbConfig().setKeyGenerator(this.applicationContext.getBean(IKeyGenerator.class));
        }

        // 注入SQL注入器
        if (this.applicationContext.getBeanNamesForType(ISqlInjector.class, false, false).length > 0) {
            globalConfig.setSqlInjector(this.applicationContext.getBean(ISqlInjector.class));
        }
        return globalConfig;
    }

    private void applyMyBatisPlusProperties(MybatisSqlSessionFactoryBean factory) {
        String configLocation = this.mybatisPlusProperties.getConfigLocation();
        if (StringUtils.hasText(configLocation)) {
            factory.setConfigLocation(this.resourceLoader.getResource(configLocation));
        }

        this.applyConfig(factory);

        Optional.ofNullable(this.mybatisPlusProperties.getConfigurationProperties())
                .ifPresent(factory::setConfigurationProperties);

        String typeAliasesPackage = this.mybatisPlusProperties.getTypeAliasesPackage();
        if (StringUtils.hasLength(typeAliasesPackage)) {
            factory.setTypeAliasesPackage(typeAliasesPackage);
        }

        Optional.ofNullable(this.mybatisPlusProperties.getTypeAliasesSuperType())
                .ifPresent(factory::setTypeAliasesSuperType);

        String typeHandlersPackage = this.mybatisPlusProperties.getTypeHandlersPackage();
        if (StringUtils.hasLength(typeHandlersPackage)) {
            factory.setTypeHandlersPackage(typeHandlersPackage);
        }

        Resource[] resources = this.mybatisPlusProperties.resolveMapperLocations();
        if (!ObjectUtils.isEmpty(resources)) {
            factory.setMapperLocations(resources);
        }

        Optional.ofNullable(this.mybatisPlusProperties.getDefaultScriptingLanguageDriver())
                .ifPresent(factory::setDefaultScriptingLanguageDriver);

        // 自定义枚举包
        String typeEnumsPackage = this.mybatisPlusProperties.getTypeEnumsPackage();
        if (StringUtils.hasLength(typeEnumsPackage)) {
            factory.setTypeEnumsPackage(typeEnumsPackage);
        }

    }

    /**
     * 应用配置
     *
     * @param factory {@link MybatisSqlSessionFactoryBean}
     */
    private void applyConfig(MybatisSqlSessionFactoryBean factory) {
        MybatisConfiguration customConfig = this.mybatisPlusProperties.getConfiguration();
        MybatisConfiguration configToApply = new MybatisConfiguration();
        // 解决多数据源时，拦截器等执行多次
        BeanUtil.copyProperties(customConfig, configToApply);

        if (!CollectionUtils.isEmpty(this.configurationCustomizers)) {
            this.configurationCustomizers.forEach(customizer -> customizer.customize(configToApply));
        }
        factory.setConfiguration(configToApply);
    }

    /**
     * 属性设置后的操作
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        if (!CollectionUtils.isEmpty(this.mybatisPlusPropertiesCustomizers)) {
            this.mybatisPlusPropertiesCustomizers.forEach(customizer ->
                    customizer.customize(this.mybatisPlusProperties));
        }
        this.checkConfigFileExists();
    }

    /**
     * 检查MyBaits Plus的配置文件是否存在
     */
    private void checkConfigFileExists() {
        String configLocation = this.mybatisPlusProperties.getConfigLocation();
        if (this.mybatisPlusProperties.isCheckConfigLocation() && StringUtils.hasText(configLocation)) {
            Resource resource = this.resourceLoader.getResource(configLocation);
            Assert.state(resource.exists(),
                    "Cannot find config location: "
                            + resource
                            + "(please add config file or check your MyBatis configuration)");
        }
    }

}
